#region Using directives
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
using System.Reflection;
using System;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers;
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Integration.WCF;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
using System.ServiceModel;
#endregion

namespace Avanade.PolicyInjection
{
    /// <summary>
    /// An <see cref="ICallHandler"/> that runs validation of a call's parameters
    /// before calling the target.
    /// </summary>
    [ConfigurationElementType(typeof(WcfValidationCallHandlerData))]
    public class WcfValidationCallHandler : ICallHandler
    {
        string ruleSet;
        int order;
        SpecificationSource specificationSource;

        /// <summary>
        /// Creates a <see cref="WcfValidationCallHandler"/> that uses the given
        /// ruleset and <see cref="SpecificationSource"/> to get the validation rules.
        /// </summary>
        /// <param name="ruleSet">Validation ruleset as specified in configuration.</param>
        /// <param name="specificationSource">Should the validation come from config, attributes, or both?</param>
        public WcfValidationCallHandler(string ruleSet, SpecificationSource specificationSource)
        {
            this.ruleSet = ruleSet;
            this.order = 0;
            this.specificationSource = specificationSource;
        }

        /// <summary>
        /// Runs the call handler. This does validation on the parameters, and if validation
        /// passes it calls the handler. It throws <see cref="ArgumentValidationException"/>
        /// if validation fails.
        /// </summary>
        /// <param name="input"><see cref="IMethodInvocation"/> containing details of the current call.</param>
        /// <param name="getNext">delegate to call to get the next handler in the pipeline.</param>
        /// <returns>Return value from the target.</returns>
        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            ValidationFault fault = new ValidationFault();
            
            for (int index = 0; index < input.Inputs.Count; ++index)
            {
                ParameterInfo inputParameter = input.Inputs.GetParameterInfo(index);
                Validator validator = CreateValidator(inputParameter);

                object parameterValue = input.Inputs[index];
                ValidationResults results = validator.Validate(parameterValue);

                AddFaultDetails(fault, inputParameter.Name, results);
            }

            if (!fault.IsValid)
            {
                FaultException<ValidationFault> faultException = new FaultException<ValidationFault>(fault);
                return input.CreateExceptionMethodReturn(faultException);
            }

            return getNext().Invoke(input, getNext);
        }

        private void AddFaultDetails(ValidationFault fault, string parameterName, ValidationResults results)
        {
            if (!results.IsValid)
            {
                foreach (ValidationResult result in results)
                {
                    fault.Add(CreateValidationDetail(result, parameterName));
                }
            }
        }

        private ValidationDetail CreateValidationDetail(ValidationResult result, string parameterName)
        {
            return new ValidationDetail(result.Message, result.Key, parameterName);
        }

        private Validator CreateValidator(ParameterInfo parameter)
        {
            Validator typeValidator = CreateValidator(parameter.ParameterType);
            Validator parameterValidator = ParameterValidatorFactory.CreateValidator(parameter);

            if (typeValidator != null)
            {
                return new AndCompositeValidator(typeValidator, parameterValidator);
            }
            return parameterValidator;
        }

        private Validator CreateValidator(Type type)
        {
            switch (specificationSource)
            {
                case SpecificationSource.Both:
                    return ValidationFactory.CreateValidator(type, ruleSet);
                case SpecificationSource.Attributes:
                    return ValidationFactory.CreateValidatorFromAttributes(type, ruleSet);
                case SpecificationSource.Configuration:
                    return ValidationFactory.CreateValidatorFromConfiguration(type, ruleSet);
                case SpecificationSource.ParameterAttributesOnly:
                default:
                    return null;
            }
        }
        
        public int Order
        {
            get
            {
                return this.order;
            }
            set
            {
                this.order = value;
            }
        }
        #region Test methods

        /// <summary>
        /// Gets the ruleset for this call handler.
        /// </summary>
        /// <value>Ruleset name.</value>
        public string RuleSet
        {
            get { return ruleSet; }
        }

        /// <summary>
        /// Source for validation information.
        /// </summary>
        /// <value>validation source.</value>
        public SpecificationSource SpecificationSource
        {
            get { return specificationSource; }
        }

        #endregion

        
    }

    /// <summary>
    /// Where should the information for validation come from?
    /// </summary>
    public enum SpecificationSource
    {
        /// <summary>
        /// Configuration and type attributes
        /// </summary>
        Both = 0,
        /// <summary>
        /// Type attributes only, ignoring configuration
        /// </summary>
        Attributes,
        /// <summary>
        /// Configuration only, ignoring type attributes
        /// </summary>
        Configuration,
        /// <summary>
        /// Only use attributes on the parameters themselves, ignoring types and configuration
        /// </summary>
        ParameterAttributesOnly
    }
}
